/*
 *  Copyright 2010 Andrew Kroh
 *
 *  Kroh-IO is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Kroh-IO is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package kroh.io;

import java.io.FileDescriptor;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;
import java.util.Enumeration;
import java.util.concurrent.CopyOnWriteArrayList;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <p>
 * Communications port management. <code>CommPortIdentifier</code> is the
 * central class for controlling access to communications ports. It includes
 * methods for:
 * </p>
 * 
 * <ul>
 * <li>Determining the communications ports made available by the driver.</li>
 * <li>Opening communications ports for I/O operations.</li>
 * <li>Determining port ownership.</li>
 * <li>Resolving port ownership contention.</li>
 * <li>Managing events that indicate changes in port ownership status.</li>
 * </ul>
 * 
 * <p>
 * An application first uses methods in <code>CommPortIdentifier</code> to
 * negotiate with the driver to discover which communication ports are available
 * and then select a port for opening. It then uses methods in other classes
 * like <code>CommPort</code>, <code>ParallelPort</code> and
 * <code>SerialPort</code> to communicate through the port.
 * </p>
 */
public class CommPortIdentifier
{
    private static final Logger logger = LoggerFactory.getLogger(CommPortIdentifier.class);
    
    public static final int PORT_SERIAL = 1;
    public static final int PORT_PARALLEL = 2;

    private final String portName;
    private final int portType;
    private boolean isAvailable;
    private String portOwner;
    private CommPort commPort;
    private CommDriver commDriver;
    
    /**
     * List of CommPortOwnershipListeners for this port. 
     */
    private final List<CommPortOwnershipListener> ownershipListeners;
    
    /**
     * Vector holding all the registered CommPorts.
     */
    private static final Vector<CommPortIdentifier> commPorts =
        new Vector<CommPortIdentifier>();
    
    static
    {
        String driverClass = System.getProperty("kroh.io.driver");
        if (driverClass == null)
        {
            driverClass = "kroh.io.KrohCommDriver";
        }
        else
        {
            driverClass = driverClass.trim();
        }
        
        try
        {
            // Load the driver implementation which will
            // populate the list of available ports:
            CommDriver commDriver = (CommDriver) 
                Class.forName(driverClass).newInstance();
            commDriver.initialize();
        } catch (Exception e)
        {
            logger.error("Unable to instantiate comm driver " +
                    "class <" + driverClass + ">.", e);
        }
    }
    
    /**
     * Constructor
     * 
     * @param name
     *            name to give this port
     * @param port
     *            instantiation of CommPort implementation
     * @param type
     *            type of port, PORT_SERIAL or PORT_PARALLEL
     * @param driver
     *            driver associated with this port
     */
    public CommPortIdentifier(String name, 
                              CommPort port, 
                              int type,
                              CommDriver driver)
    {
        portName = name;
        portType = type;
        isAvailable = true;
        commPort = port;
        commDriver = driver;
        
        ownershipListeners = new CopyOnWriteArrayList<CommPortOwnershipListener>();
    }

    /**
     * Obtains an enumeration object that contains a CommPortIdentifier object
     * for each port in the system.
     * 
     * @return Enumeration that can be used to enumerate all the ports known to
     *         the system
     */
    public static Enumeration<CommPortIdentifier> getPortIdentifiers()
    {
        return commPorts.elements();
    }

    /**
     * Obtains a CommPortIdentifier object by using a port name. The port name
     * may have been stored in persistent storage by the application.
     * 
     * @param portName
     *            name of the port to obtain
     * @return the CommPortIdentifier object representing the port
     * 
     * @throws NoSuchPortException
     *             if the port does not exist
     */
    public static CommPortIdentifier getPortIdentifier(String portName)
            throws NoSuchPortException
    {
        for (CommPortIdentifier id : commPorts)
        {
            if (id.getName().equals(portName))
            {
                return id;
            }
        }
        
        throw new NoSuchPortException("Port <" + portName + "> not found.");
    }

    /**
     * Obtains the CommPortIdentifier object corresponding to a port that has
     * already been opened by the application.
     * 
     * @param port
     *            a CommPort object obtained from a previous open
     * 
     * @return the CommPortIdentifier object representing the port
     * 
     * @throws NoSuchPortException
     *             if the port object is invalid
     */
    public static CommPortIdentifier getPortIdentifier(CommPort port)
            throws NoSuchPortException
    {
        if (port == null)
        {
            throw new NoSuchPortException("CommPort is invalid (null).");
        }
        
        for (CommPortIdentifier id : commPorts)
        {
            synchronized (id)
            {
                if (id.commPort != null && id.commPort.equals(port))
                {
                    return id;
                }
            }
        }
        
        throw new NoSuchPortException("CommPortIdentifer for the specified " +
                "CommPort was not found.");
    }

    /**
     * Adds portName to the list of ports. 
     * 
     * @param portName
     *          name of the port being added
     * @param portType
     *          type of the port being added
     * @param driver
     *          the driver representing the port being added
     */
    public static void addPortName(String portName, int portType, CommDriver driver)
    {
        commPorts.add(new CommPortIdentifier(portName, null, portType, driver));
    }

    /**
     * Opens the communications port. <code>open</code> obtains exclusive
     * ownership of the port. If the port is owned by some other application, a
     * <code>PORT_OWNERSHIP_REQUESTED</code> event is propagated using the
     * <code>CommPortOwnershipListener</code> event mechanism. If the
     * application that owns the port calls <code>close</code> during the event
     * processing, then this <code>open</code> will succeed.
     * 
     * <p>
     * There is one <code>InputStream</code> and one <code>OutputStream</code>
     * associated with each port. After a port is opened with <code>open</code>,
     * then all calls to <code>getInputStream</code> will return the same stream
     * object until <code>close</code> is called.
     * 
     * @param appname
     *            Name of application making this call. This name will become
     *            the owner of the port. Useful when resolving ownership
     *            contention.
     * @param timeout
     *            Time in milliseconds to block waiting for port open.
     * 
     * @return CommPort object
     * 
     * @throws PortInUseException
     *             if the port is in use by some other application that is not
     *             willing to relinquish ownership
     */
    public CommPort open(String appname, int timeout)
            throws PortInUseException
    {
        boolean interrupted = false;
        
        try
        {
            synchronized (this)
            {
                if (!isAvailable)
                {
                    fireOwnershipEvent(CommPortOwnershipListener.PORT_OWNERSHIP_REQUESTED);
                }
                
                final long startTime = System.currentTimeMillis();
                while (!isAvailable && timeout < (System.currentTimeMillis() - startTime))
                {
                    try
                    {
                        wait(timeout);
                    }
                    catch (InterruptedException e)
                    {
                        interrupted = true;
                    }
                }
                
                if (isAvailable)
                {
                    if (commPort == null)
                    {
                        commPort = commDriver.getCommPort(portName, portType);
                    }

                    isAvailable = false;
                    portOwner = appname;
                    fireOwnershipEvent(CommPortOwnershipListener.PORT_OWNED);
                    
                    return commPort;
                }
                else
                {
                    throw new PortInUseException("Port <" + getName() + 
                            "> is in use by <" + getCurrentOwner() + ">.");
                }
            }
        }
        finally
        {
            if (interrupted)
            {
                Thread.currentThread().interrupt();
            }
        }
    }

    /**
     * Opens the communications port using a FileDescriptor object on platforms
     * that support this technique.
     * 
     * <p>
     * Not supported by this implementation.
     * 
     * @param fd
     *            FileDescriptor associated with this CommPort
     * @return CommPort object
     * 
     * @throws UnsupportedCommOperationException
     *             on platforms which do not support this functionality.
     */
    public CommPort open(FileDescriptor f)
            throws UnsupportedCommOperationException
    {
        throw new UnsupportedCommOperationException();
    }

    /**
     * Returns the name of the port.
     * 
     * @return the name of the port
     */
    public String getName()
    {
        return portName;
    }

    /**
     * Returns the port type.
     * 
     * @return type of port, PORT_SERIAL or PORT_PARALLEL
     */
    public int getPortType()
    {
        return portType;
    }

    /**
     * Returns the owner of the port.
     * 
     * @return current owner of the port
     */
    public String getCurrentOwner()
    {
        synchronized (this)
        {
            return portOwner;
        }
    }

    /**
     * Checks whether the port is owned.
     * 
     * @return boolean true if port is owned by an application, false
     *      if the port is not owned
     */
    public boolean isCurrentlyOwned()
    {
        synchronized (this)
        {
            return !isAvailable;
        }
    }

    /**
     * Registers an interested application so that it can receive notification
     * of changes in port ownership. This includes notification of the following
     * events:
     * 
     * <ul>
     * <li> <code>PORT_OWNED</code>: Port became owned</li>
     * <li> <code>PORT_UNOWNED</code>: Port became unowned</li>
     * <li> <code>PORT_OWNERSHIP_REQUESTED</code> If the application owns this
     * port and is willing to give up ownership, then it should call
     * <code>close</code> now.</li>
     * </ul>
     * 
     * The <code>ownershipChange</code> method of the listener registered using
     * <code>addPortOwnershipListener</code> will be called with one of the
     * above events.
     * 
     * @param listener
     *          CommPortOwnershipListener callback object
     */
    public void addPortOwnershipListener(CommPortOwnershipListener listener)
    {
        ownershipListeners.add(listener);
    }

    /**
     * Unregisters a CommPortOwnershipListener registered using
     * addPortOwnershipListener.
     * 
     * @param listener
     *            The CommPortOwnershipListener object that was previously
     *            registered using addPortOwnershipListener
     */
    public void removePortOwnershipListener(CommPortOwnershipListener listener)
    {
        ownershipListeners.remove(listener);
    }
    
    // ------------------------------------------------------------------------
    //          Implementation specific methods.
    // ------------------------------------------------------------------------
    
    /**
     * Marks the port as classed and fires an ownership event to
     * registered listeners. 
     * 
     * <p>
     * Note: The method is marked as package-private
     * and is for internal use only.
     */
    void close()
    {
        synchronized (this)
        {
            isAvailable = true;
            portOwner = null;
            commPort = null;
            
            fireOwnershipEvent(CommPortOwnershipListener.PORT_UNOWNED);
            notifyAll();
        }
    }
    
    /**
     * Fires an ownership event to all registered listeners.
     * 
     * @param eventType
     *          type of event to fire
     */
    private void fireOwnershipEvent(final int eventType)
    {
        // Thread off the notifications to prevent any
        // possible deadlock:
        new Timer().schedule(new TimerTask()
        {
            @Override
            public void run()
            {
                for (CommPortOwnershipListener l : ownershipListeners)
                {
                    l.ownershipChange(eventType);
                }
            }
        }, 0);
    }
}
